最終確認日

GitHub Actionsを使ってSupabaseにObsidianの記事をアップロードする

FreshでObsidian Publishライクなブログを目指すより。

やること

手順

  1. Supabaseに登録する
    • 料金FREEの範囲内であれば嬉しい。ダメなら$25/月。
  2. テーブルを考える
  3. upload_to_supabase.ts ファイルを作り、ローカルでSupabaseにアップロードできるようにしてみる
  4. GitHub Actionsを使ってSupabaseにアップロードする(3を実行)

テーブル

ブログ記事のテーブル。基本的にコレまでのブログの構造を基に、ノートの値もカバーできるようにした。

CREATE TABLE articles (
  id SERIAL PRIMARY KEY,
  slug TEXT UNIQUE NOT NULL,  -- 記事の識別子
  title TEXT NOT NULL,        -- 記事のタイトル
  content TEXT NOT NULL,      -- Markdown の内容
  created_at TIMESTAMP DEFAULT NOW(),
  updated_at TIMESTAMP DEFAULT NOW(),
  private BOOLEAN DEFAULT false, -- 非公開フラグ
  aliases TEXT[] DEFAULT ARRAY[]::TEXT[], -- 別名
  url TEXT DEFAULT '', -- 外部ソースのリンク
  cover_image TEXT DEFAULT NULL  -- カバー画像
);

GitHub Actionsを使ってSupabaseにObsidianの記事をアップロードする-1742400302465

ぽちぽち作る

GitHub Actionsを使ってSupabaseにObsidianの記事をアップロードする-1742400317192

  • RLSとは
  • aliases が作れない
  • Table EditorじゃなくてSQL Editor を使えば良さそう。
  • 1回 Table Editorで作ったものを削除して、SQL Editor で実行。

GitHub Actionsを使ってSupabaseにObsidianの記事をアップロードする-1742400617125

  • Table Editor側で見てもできている。

GitHub Actionsを使ってSupabaseにアップロードする

スクリプトを書く。 アップロードしたい保管庫.deno/upload_to_supabase.ts に配置。

.deno
├── deno.json
└── upload_to_supabase.ts
{
    "compilerOptions": {
        "strict": true
    },
    "tasks": {
        "upload": "deno run --allow-read --allow-net --allow-env .deno/upload_to_supabase.ts"
    },
    "imports": {},
    "lint": {
        "rules": {
            "recommended": true
        }
    }
}

upload_to_supabase.ts は最後に書く。

upload_to_supabase.ts
import "https://deno.land/std@0.224.0/dotenv/load.ts";
import { createClient } from "https://esm.sh/@supabase/supabase-js";
import { parse } from "https://deno.land/std/yaml/mod.ts";

const SUPABASE_URL = Deno.env.get("SUPABASE_URL")!;
const SUPABASE_KEY = Deno.env.get("SUPABASE_KEY")!;
const supabase = createClient(SUPABASE_URL, SUPABASE_KEY);

// アップロード対象フォルダ
const uploadFolders = ["Notes", "100_DailyNote"];

interface Metadata {
  created_at: string;
  updated_at: string;
  private: boolean;
  aliases: string[];
  url: string;
  cover_image?: string;
}

// Markdown のメタデータと内容をパース
function parseMarkdown(markdown: string): { metadata: Metadata; content: string } {
  const match = markdown.match(/^---\n([\s\S]+?)\n---/);
  if (!match) throw new Error("Metadata not found");

  const metadata = parse(match[1]) as Metadata;
  const content = markdown.replace(match[0], "").trim();
  return { metadata, content };
}

// 指定フォルダ内のファイルを取得
async function getMarkdownFiles(): Promise<string[]> {
  const files: string[] = [];

  for (const folder of uploadFolders) {
    try {
      for await (const file of Deno.readDir(`./content/${folder}`)) {
        if (file.name.endsWith(".md")) {
          files.push(`./content/${folder}/${file.name}`);
        }
      }
    } catch (error) {
      console.warn(`Skipping ${folder}: ${(error as Error).message}`);
    }
  }

  return files;
}

// 記事のアップロード処理
async function uploadArticles() {
  const markdownFiles = await getMarkdownFiles();

  for (const filePath of markdownFiles) {
    const fileName = filePath.split("/").pop()!;
    const slug = fileName.replace(".md", "");

    try {
      const markdown = await Deno.readTextFile(filePath);
      const { metadata, content } = parseMarkdown(markdown);

      // `private: true` の記事はスキップ
      if (metadata.private) {
        console.log(`Skipping private article: ${slug}`);
        continue;
      }

      console.log(`Uploading article: ${slug}`);

      const { error } = await supabase.from("articles").upsert([
        {
          slug,
          title: slug.replace(/-/g, " "),
          content,
          created_at: metadata.created_at,
          updated_at: metadata.updated_at,
          private: metadata.private,
          aliases: metadata.aliases,
          url: metadata.url,
          cover_image: metadata.cover_image || null,
        }
      ]);

      if (error) console.error(`Error uploading ${slug}:`, error.message);
    } catch (err) {
      console.error(`Error processing file ${filePath}:`, (err as Error).message);
    }
  }
}

// 実行
await uploadArticles();

一回ローカルで実行してみる。.env を作成して .gitignore に含めておく。(.deno内ではなく外に置いておく)

.env
SUPABASE_URL=https://your-supabase-instance.supabase.co
SUPABASE_KEY=your-service-role-key
deno run --allow-read --allow-net --allow-env .deno/upload_to_supabase.ts

次のようなエラーが出た。

Uploading article: 2018-end Error uploading 2018-end: new row violates row-level security policy for table "articles"

RLSの設定をしているかららしい。SUPABASE_KEY に設定する値を管理者キーに変更する。

GitHub Actionsを使ってSupabaseにObsidianの記事をアップロードする-1742412677016

アップロードできた。330記事あるらしい。

2回目に実行すると次のようになる。

Error uploading vvv-3-2-wordpress: duplicate key value violates unique constraint "articles_slug_key"

同じ slug がある場合には上書きするようにする。

const { error } = await supabase.from("articles").upsert([
  {
    slug,
    title: title,
    content,
    created_at: metadata.createdDate,
    updated_at: metadata.updatedDate,
    private: metadata.private,
    aliases: metadata.aliases,
    url: metadata.url,
    cover_image: coverImagePath,
  }
], { onConflict: ["slug"] });  // ← onConflict を追加

画像をアップロードする

まだSupabase側で何もやってないからか、Bucket not foundになった。

GitHub Actionsを使ってSupabaseにObsidianの記事をアップロードする-1742413405255

作った。

  • public と private が選べた
    • Fresh側でよしなにするのでprivateにした。

private の場合は特殊でSignedURLを取得して、重複がないかを確認しないといけない。

async function getSignedUrl(fileName: string): Promise<string | null> {
  const safeFileName = slugifyFileName(fileName);
  const { data, error } = await supabase.storage.from(bucketName).createSignedUrl(safeFileName, 60 * 60);

  if (error) {
    console.error(`Error generating signed URL for ${safeFileName}:`, (error as Error).message);
    return null;
  }

  return data.signedUrl;
}

Error uploading image: 2015-11-このブログの制作手順とかについてザックリと-9b59f04635264ca7b01b07d4cc3d8d5a.jpg: Invalid key: 2015-11-このブログの制作手順とかについてザックリと-9b59f04635264ca7b01b07d4cc3d8d5a.jpg

特殊文字や日本語が入っているとだめらしい。

ファイル名をASCIIにする関数を追加。

function slugifyFileName(fileName: string): string {
  return fileName
    .normalize("NFKD") // Unicode 正規化(濁点・半濁点を分離)
    .replace(/[\u0300-\u036f]/g, "") // ダイアクリティカルマーク除去
    .replace(/[^\w.-]/g, "_") // 許可されていない文字を `_` に変換
    .replace(/_{2,}/g, "_") // 連続する `_` を1つに
    .toLowerCase();
}

GitHub Actionsの設定

  • GitHub Secrets を設定する

    • SUPABASE_URL
    • SUPABASE_KEY GitHub Actionsを使ってSupabaseにObsidianの記事をアップロードする-1742415894683
  • .github/workflows/upload-to-supabase.yml を作る

ここまでのコード

upload_to_supabase.ts
import "https://deno.land/std@0.224.0/dotenv/load.ts";
import { createClient } from "https://esm.sh/@supabase/supabase-js";
import { parse } from "https://deno.land/std/yaml/mod.ts";

const SUPABASE_URL = Deno.env.get("SUPABASE_URL")!;
const SUPABASE_KEY = Deno.env.get("SUPABASE_KEY")!;
const supabase = createClient(SUPABASE_URL, SUPABASE_KEY);

// アップロード対象のフォルダ(Markdown と画像)
const markdownFolders = ["010_Blog"];
const imageFolders = ["011_BlogImages"];
const imageExtensions = [".jpg", ".jpeg", ".png", ".webp", ".gif"];
const bucketName = "blog-images"; // 非公開バケット名

// Obsidian の画像埋め込み `![[filename|widthxheight]]`
const obsidianImageRegex = /!\[\[(.*?)\|?(\d*x\d*)?\]\]/g;
// リンク内の画像 `[![](../path/to/image.jpg)](link)`
const nestedImageRegex = /\[\!\[\]\((.*?)\)\]\(.*?\)/g;

interface Metadata {
  title?: string;
  slug?: string;
  createdDate: string;
  updatedDate: string;
  private: boolean;
  aliases: string[];
  url: string;
  coverImage?: string;
}

// Markdown のメタデータと内容をパース
function parseMarkdown(
  markdown: string,
): { metadata: Metadata; content: string } {
  const match = markdown.match(/^---\n([\s\S]+?)\n---/);
  if (!match) throw new Error("Metadata not found");

  const metadata = parse(match[1]) as Metadata;
  const content = markdown.replace(match[0], "").trim();
  return { metadata, content };
}

// `signed URL` を取得する関数
async function getSignedUrl(fileName: string): Promise<string | null> {
  const safeFileName = slugifyFileName(fileName);
  const { data, error } = await supabase.storage.from(bucketName).createSignedUrl(safeFileName, 60 * 60);

  if (error) {
    console.error(`Error generating signed URL for ${safeFileName}:`, (error as Error).message);
    return null;
  }

  return data.signedUrl;
}


// ファイル名を slugify して ASCII のみの形式に変換
function slugifyFileName(fileName: string): string {
  return fileName
    .normalize("NFKD") // Unicode 正規化(濁点・半濁点を分離)
    .replace(/[\u0300-\u036f]/g, "") // ダイアクリティカルマーク除去
    .replace(/[^\w.-]/g, "_") // 許可されていない文字を `_` に変換
    .replace(/_{2,}/g, "_") // 連続する `_` を1つに
    .toLowerCase();
}

// 画像を Supabase Storage にアップロード(変更があった場合のみ)
async function uploadImageToSupabase(filePath: string): Promise<string | null> {
  const fileName = filePath.split("/").pop();
  if (!fileName) {
    console.error(`Invalid file name from ${filePath}`);
    return null;
  }

  // ASCII 形式に変換
  const safeFileName = slugifyFileName(fileName);

  try {
    const signedUrl = await getSignedUrl(safeFileName);
    if (signedUrl) {
      console.log(`Skipping unchanged image: ${fileName}`);
      return signedUrl;
    }

    const fileData = await Deno.readFile(filePath);
    const { error } = await supabase.storage.from(bucketName).upload(safeFileName, fileData, {
      upsert: true,
      contentType: getContentType(filePath),
    });

    if (error) {
      console.error(`Error uploading image: ${fileName}:`, error.message);
      return null;
    }

    console.log(`Uploaded image: ${fileName}`);
    return await getSignedUrl(safeFileName);
  } catch (error) {
    console.error(`Error reading file ${filePath}:`, (error as Error).message);
    return null;
  }
}

// 画像のコンテンツタイプを取得
function getContentType(filePath: string): string {
  const ext = filePath.slice(filePath.lastIndexOf(".")).toLowerCase();
  switch (ext) {
    case ".jpg":
    case ".jpeg":
      return "image/jpeg";
    case ".png":
      return "image/png";
    case ".webp":
      return "image/webp";
    case ".gif":
      return "image/gif";
    default:
      return "application/octet-stream";
  }
}

// 画像を処理する関数
async function processImage(imageUrl: string): Promise<string | null> {
  const fileName = imageUrl.split("/").pop();
  if (!fileName) {
    console.error(`Invalid file name extracted from ${imageUrl}`);
    return null;
  }

  for (const folder of imageFolders) {
    const imagePath = `./${folder}/${fileName}`; // 既存の拡張子をそのまま利用
    try {
      await Deno.stat(imagePath);
      console.log(`Processing image: ${imagePath}`);
      return await uploadImageToSupabase(imagePath);
    } catch {
      continue;
    }
  }

  console.error(`File not found for ${imageUrl}`);
  return null;
}


// 指定フォルダ内の Markdown ファイルを取得
async function getMarkdownFiles(): Promise<string[]> {
  const files: string[] = [];

  for (const folder of markdownFolders) {
    try {
      for await (const file of Deno.readDir(`./${folder}`)) {
        if (file.name.endsWith(".md")) {
          files.push(`./${folder}/${file.name}`);
        }
      }
    } catch (error) {
      console.warn(`Skipping ${folder}: ${(error as Error).message}`);
    }
  }

  return files;
}

// 記事のアップロード処理
async function uploadArticles() {
  const markdownFiles = await getMarkdownFiles();

  for (const filePath of markdownFiles) {
    try {
      const markdown = await Deno.readTextFile(filePath);
      const { metadata, content } = parseMarkdown(markdown);
      const fileName = filePath.split("/").pop()!;
      const slug = metadata.slug ?? fileName.replace(".md", "");
      const title = metadata.title ?? slug.replace(/-/g, " ");

      // `private: true` の記事はスキップ
      if (metadata.private) {
        console.log(`Skipping private article: ${slug}`);
        continue;
      }

      // coverImage の signed URL を取得
      const coverImagePath = metadata.coverImage
        ? await processImage(metadata.coverImage)
        : null;

      // Obsidian 形式の画像を処理
      let match;
      while ((match = obsidianImageRegex.exec(content)) !== null) {
        const imageUrl = match[1];
        await processImage(imageUrl);
      }

      // リンク内の画像を処理
      while ((match = nestedImageRegex.exec(content)) !== null) {
        const imageUrl = match[1];
        await processImage(imageUrl);
      }

      console.log(`Uploading article: ${slug}`);
      const data = {
        slug,
        title: title,
        content, // content は変更せずそのまま保存
        created_at: metadata.createdDate,
        updated_at: metadata.updatedDate,
        private: metadata.private,
        aliases: metadata.aliases,
        url: metadata.url,
        cover_image: coverImagePath, // signed URL を保存
      };

      const { error } = await supabase.from("articles").upsert([data], { onConflict: ["slug"] });

      if (error) {
        console.error(`Error uploading ${slug}:`, (error as Error).message);
      }
    } catch (err) {
      console.error(
        `Error processing file ${filePath}:`,
        (err as Error).message,
      );
    }
  }
}

// 実行
await uploadArticles();
.github/workflows/upload-to-supabase.yml
name: Upload to Supabase

on:
  push:
    branches:
      - main  # `main` ブランチに push された時に実行

jobs:
  upload:
    runs-on: ubuntu-latest

    steps:
      - name: Checkout repository
        uses: actions/checkout@v4

      - name: Install Deno
        uses: denoland/setup-deno@v2
        with:
          deno-version: v2.x

      - name: Run upload script
        env:
          SUPABASE_URL: ${{ secrets.SUPABASE_URL }}
          SUPABASE_KEY: ${{ secrets.SUPABASE_KEY }}
        run: |
          deno run --allow-read --allow-net --allow-env .deno/upload_to_supabase.ts

問題点

  • mainブランチに変更があった時に全ての画像とノートをアップロードし始める
    • 変更があったものだけで良い
    • git diff で変更のあったファイルのみを抽出する
    • めっちゃ苦戦した。

修正後に次のようなエラーが出た。

fatal: ambiguous argument 'HEAD^': unknown revision or path not in the working tree. Use '--' to separate paths from revisions, like this:

GitHub Actions で git diff HEAD^ HEAD を実行した際に HEAD^ が見つからない ことが原因。GitHub Actions の checkout はデフォルトで 1 つのコミットのみを取得するため、履歴がない。

最終コード

upload_to_supabase.ts
import "https://deno.land/std@0.224.0/dotenv/load.ts";
import { createClient } from "https://esm.sh/@supabase/supabase-js";
import { parse } from "https://deno.land/std/yaml/mod.ts";

const SUPABASE_URL = Deno.env.get("SUPABASE_URL")!;
const SUPABASE_KEY = Deno.env.get("SUPABASE_KEY")!;
const supabase = createClient(SUPABASE_URL, SUPABASE_KEY);

// アップロード対象のフォルダ(Markdown と画像)
const markdownFolders = ["010_Blog"];
const imageFolders = ["011_BlogImages"];
const bucketName = "blog-images"; // 非公開バケット名

// Obsidian の画像埋め込み `![[filename|widthxheight]]`
const obsidianImageRegex = /!\[\[(.*?)\|?(\d*x\d*)?\]\]/g;
// リンク内の画像 `[![](../path/to/image.jpg)](link)`
const nestedImageRegex = /\[\!\[\]\((.*?)\)\]\(.*?\)/g;

interface Metadata {
  title?: string;
  slug?: string;
  createdDate: string;
  updatedDate: string;
  private: boolean;
  aliases: string[];
  url: string;
  coverImage?: string;
}

// 変更されたファイルを `changed_files.txt` から取得
async function getChangedFiles(): Promise<string[]> {
  try {
    const fileList = await Deno.readTextFile("changed_files.txt");
    return fileList.trim()
      .split("\n")
      // deno-lint-ignore no-control-regex
      .map((file) => file.replace(/\x00/g, "").trim()); // 🔥 NULL文字 (\x00) を削除
  } catch (error) {
    console.error("Error reading changed_files.txt:", (error as Error).message);
    return [];
  }
}

// **ファイルが指定フォルダに属しているかチェック**
function isValidFilePath(filePath: string, validFolders: string[]): boolean {
  return validFolders.some((folder) => filePath.startsWith(folder + "/"));
}

// Markdown のメタデータと内容をパース
function parseMarkdown(
  markdown: string,
): { metadata: Metadata; content: string } {
  const match = markdown.match(/^---\n([\s\S]+?)\n---/);
  if (!match) throw new Error("Metadata not found");

  const metadata = parse(match[1]) as Metadata;
  const content = markdown.replace(match[0], "").trim();
  return { metadata, content };
}

// `signed URL` を取得する関数
async function getSignedUrl(fileName: string): Promise<string | null> {
  const safeFileName = slugifyFileName(fileName);
  const { data, error } = await supabase.storage.from(bucketName)
    .createSignedUrl(safeFileName, 60 * 60);

  if (error) {
    console.error(
      `Error generating signed URL for ${safeFileName}:`,
      (error as Error).message,
    );
    return null;
  }

  return data.signedUrl;
}

// ファイル名を slugify して ASCII のみの形式に変換
function slugifyFileName(fileName: string): string {
  return fileName
    .normalize("NFKD") // Unicode 正規化(濁点・半濁点を分離)
    .replace(/[\u0300-\u036f]/g, "") // ダイアクリティカルマーク除去
    .replace(/[^\w.-]/g, "_") // 許可されていない文字を `_` に変換
    .replace(/_{2,}/g, "_") // 連続する `_` を1つに
    .toLowerCase();
}

// 画像を Supabase Storage にアップロード(変更があった場合のみ)
async function uploadImageToSupabase(filePath: string): Promise<string | null> {
  const fileName = filePath.split("/").pop();
  if (!fileName) {
    console.error(`Invalid file name from ${filePath}`);
    return null;
  }

  // ASCII 形式に変換
  const safeFileName = slugifyFileName(fileName);

  try {
    const signedUrl = await getSignedUrl(safeFileName);
    if (signedUrl) {
      console.log(`Skipping unchanged image: ${fileName}`);
      return signedUrl;
    }

    const fileData = await Deno.readFile(filePath);
    const { error } = await supabase.storage.from(bucketName).upload(
      safeFileName,
      fileData,
      {
        upsert: true,
        contentType: getContentType(filePath),
      },
    );

    if (error) {
      console.error(`Error uploading image: ${fileName}:`, error.message);
      return null;
    }

    console.log(`Uploaded image: ${fileName}`);
    return await getSignedUrl(safeFileName);
  } catch (error) {
    console.error(`Error reading file ${filePath}:`, (error as Error).message);
    return null;
  }
}

// 画像のコンテンツタイプを取得
function getContentType(filePath: string): string {
  const ext = filePath.slice(filePath.lastIndexOf(".")).toLowerCase();
  switch (ext) {
    case ".jpg":
    case ".jpeg":
      return "image/jpeg";
    case ".png":
      return "image/png";
    case ".webp":
      return "image/webp";
    case ".gif":
      return "image/gif";
    default:
      return "application/octet-stream";
  }
}

// 画像を処理する関数
async function processImage(imageUrl: string): Promise<string | null> {
  const fileName = imageUrl.split("/").pop();
  if (!fileName) {
    console.error(`Invalid file name extracted from ${imageUrl}`);
    return null;
  }

  for (const folder of imageFolders) {
    const imagePath = `./${folder}/${fileName}`; // 既存の拡張子をそのまま利用
    try {
      await Deno.stat(imagePath);
      console.log(`Processing image: ${imagePath}`);
      return await uploadImageToSupabase(imagePath);
    } catch {
      continue;
    }
  }

  console.error(`File not found for ${imageUrl}`);
  return null;
}

// 記事が Supabase に存在するか確認する関数
async function checkIfArticleExists(slug: string): Promise<boolean> {
  const { data, error } = await supabase
    .from("articles")
    .select("slug")
    .eq("slug", slug)
    .single();

  if (error) {
    return false; // 記事が存在しない
  }
  return !!data;
}

// 記事のアップロード処理
async function uploadArticle(filePath: string) {
  try {
    const markdown = await Deno.readTextFile(filePath);
    const { metadata, content } = parseMarkdown(markdown);
    const fileName = filePath.split("/").pop()!;
    const slug = metadata.slug ?? fileName.replace(".md", "");
    const title = metadata.title ?? slug.replace(/-/g, " ");

    if (!isValidFilePath(filePath, markdownFolders)) {
      console.warn(`Skipping markdown outside valid folders: ${filePath}`);
      return;
    }

    // `private: true` の場合の処理
    if (metadata.private) {
      const exists = await checkIfArticleExists(slug);
      if (exists) {
        console.log(`Updating existing article as private: ${slug}`);

        const { error } = await supabase
          .from("articles")
          .update({ private: true })
          .eq("slug", slug);

        if (error) {
          console.error(
            `Error updating article as private: ${slug}`,
            error.message,
          );
        }
      } else {
        console.log(`Skipping new private article: ${slug}`);
      }
      return;
    }

    // coverImage の signed URL を取得
    const coverImagePath = metadata.coverImage
      ? await processImage(metadata.coverImage)
      : null;

    // Obsidian 形式の画像を処理
    let match;
    while ((match = obsidianImageRegex.exec(content)) !== null) {
      const imageUrl = match[1];
      await processImage(imageUrl);
    }

    // リンク内の画像を処理
    while ((match = nestedImageRegex.exec(content)) !== null) {
      const imageUrl = match[1];
      await processImage(imageUrl);
    }

    console.log(`Uploading article: ${slug}`);
    const data = {
      slug,
      title: title,
      content, // content は変更せずそのまま保存
      created_at: metadata.createdDate,
      updated_at: metadata.updatedDate,
      private: metadata.private,
      aliases: metadata.aliases,
      url: metadata.url,
      cover_image: coverImagePath, // signed URL を保存
    };

    const { error } = await supabase.from("articles").upsert([data], {
      onConflict: ["slug"],
    });

    if (error) {
      console.error(`Error uploading ${slug}:`, (error as Error).message);
    }
  } catch (err) {
    console.error(
      `Error processing file ${filePath}:`,
      (err as Error).message,
    );
  }
}

// 変更されたファイルを処理する関数
async function uploadChangedFiles() {
  // 変更されたファイルのみを受け取る
  const changedFiles = await getChangedFiles();
  console.log("changedFiles:", changedFiles);
  for (const filePath of changedFiles) {
    if (filePath.endsWith(".md")) {
      await uploadArticle(filePath);
    } else if (/\.(jpg|jpeg|png|webp|gif)$/.test(filePath)) {
      if (!isValidFilePath(filePath, imageFolders)) {
        await processImage(filePath);
      }
    }
  }
}

// 実行
await uploadChangedFiles();
.github/workflows/upload-to-supabase.yml
name: Upload to Supabase

on:
  push:
    branches:
      - main

jobs:
  upload:
    runs-on: ubuntu-latest

    steps:
      - name: Checkout repository
        uses: actions/checkout@v4
        with:
          fetch-depth: 2  # 最新の2コミットを取得して git diff が使えるようにする

      - name: Get changed files
        id: changed-files
        run: |
          if git rev-parse HEAD~1 >/dev/null 2>&1; then
            CHANGED_FILES=$(git diff --name-only -z --diff-filter=AM HEAD~1 HEAD | tr '\0' '\n' | grep -E '\.(md|jpg|jpeg|png|webp|gif)$' || true)
          else
            CHANGED_FILES=$(git diff --name-only -z --diff-filter=AM HEAD | tr '\0' '\n' | grep -E '\.(md|jpg|jpeg|png|webp|gif)$' || true)
          fi
      
          if [[ -n "$CHANGED_FILES" ]]; then
            {
              echo "CHANGED_FILES<<EOF"
              echo "$CHANGED_FILES"
              echo "EOF"
            } >> "$GITHUB_ENV"
          else
            echo "CHANGED_FILES=none" >> "$GITHUB_ENV"
          fi
    
      - name: Install Deno
        if: env.CHANGED_FILES != 'none'
        uses: denoland/setup-deno@v2
        with:
          deno-version: v2.x
  
      - name: Upload changed files to Supabase
        if: env.CHANGED_FILES != 'none'
        env:
          SUPABASE_URL: ${{ secrets.SUPABASE_URL }}
          SUPABASE_KEY: ${{ secrets.SUPABASE_KEY }}
          CHANGED_FILES: ${{ env.CHANGED_FILES }}
        run: |
          echo -e "$CHANGED_FILES" > changed_files.txt
          deno run --allow-read --allow-net --allow-env .deno/upload_to_supabase.ts changed_files.txt
サイトアイコン
公開日
更新日